/*
 * Copyright (C) 2013 SeekDeepWithin.com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.seekdeepwithin.Controls;

import com.seekdeepwithin.*;
import com.seekdeepwithin.Data.*;
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import javafx.scene.control.*;

/**
 * User: Jonathan Montiverdi
 * Date: Oct 1, 2013
 *
 * Renders a list of given passages into HTML.
 */
public class PassageRenderer
{
   private Map<Integer, Integer> m_Insertions;
   private StringBuilder m_FooterTable;
   private boolean m_UseFooterCount;
   private int m_FooterCount;

   /**
    * Initializes a new passage renderer.
    */
   public PassageRenderer ()
   {
      this.m_FooterCount = 1;
      this.m_UseFooterCount = true;
      this.m_Insertions = new HashMap<> ();
      this.m_FooterTable = new StringBuilder ();
   }

   /**
    * Sets if we should use the footer count to denote footers or an asterisk (*).
    * @param useFooterCount
    */
   public void setUserFooterCount (boolean useFooterCount)
   {
      this.m_UseFooterCount = useFooterCount;
   }

   private StringBuilder insert (StringBuilder text, int index, String insertion)
   {
      int addedLength = this.getAddedLength (index);
      this.addInsertion (index, insertion.length ());
      return text.insert (index + addedLength, insertion);
   }

   /**
    * Renders the given passage.
    * @param passage Passage to render.
    * @return The HTML representation of the given passage.
    */
   public String render (Passage passage)
   {
      this.m_Insertions.clear ();
      StringBuilder passageHtml = new StringBuilder (passage.getText ());
      List<Link> definitions = passage.getDefinitions ();
      if (definitions != null) {
         for (Link definition : definitions) {
            String insertion = MessageFormat.format ("<span title=\"{0}\" data-itemid=\"{1}\" class=\"definition\">",
                                                     definition.getName (), definition.getId ());
            passageHtml = this.insert (passageHtml, definition.getStartIndex (), insertion);
            passageHtml = this.insert (passageHtml, definition.getEndIndex (), "</span>");
         }
      }

      List<Link> links = passage.getLinks ();
      if (links != null) {
         for (Link link : links) {
            passageHtml = this.insert (passageHtml, link.getStartIndex (), renderLinkStart (link));
            passageHtml = this.insert (passageHtml, link.getEndIndex (), "</a>");
         }
      }
      List<Style> styles = passage.getStyles ();
      if (styles != null) {
         for (Style styleItem : styles) {
            String style = styleItem.getStyle ();
            String styleType = styleItem.getStyleType ();
            boolean isBreak = styleType.equalsIgnoreCase ("br");
            boolean isAnchor = styleType.equalsIgnoreCase ("anchor");
            if (isAnchor) {
               styleType = "span";
               style = " class=\"anchor\" id=\"" + style + "\"";
            } else if (!style.isEmpty ()) {
               style = " class=\"" + style + "\"";
            }
            String insertion = "<" + styleType + style;
            if (isBreak) {
               insertion += " />";
            } else if (isAnchor) {
               insertion += "></span>";
            } else {
               insertion += ">";
            }
            passageHtml = this.insert (passageHtml, styleItem.getStartIndex (), insertion);
            if (!isBreak && !isAnchor) {
               passageHtml = this.insert (passageHtml, styleItem.getEndIndex (), "</" + styleType + ">");
            }
         }
      }

      Map<Integer, BaseDbObject> footers = passage.getFooters ();
      if (footers != null) {
         if (this.m_FooterTable.length () <= 0) {
            this.m_FooterTable.append ("<table class=\"footer\"><tr><th colspan=\"2\" class=\"alnleft\">notes:</th></tr>")
                    .append (Html.Newline);
         }
         if (passage.getNumber () == -1) {
            this.m_FooterTable.append ("<tr><th>&#8593;</th><td><ul>").append (Html.Newline);
         } else {
            this.m_FooterTable.append ("<tr><th>")
                    .append (passage.getNumber ())
                    .append ("</th><td><ul>")
                    .append (Html.Newline);
         }

         for (Map.Entry<Integer, BaseDbObject> footerItem : footers.entrySet ()) {
            Integer index = footerItem.getKey ();
            BaseDbObject footer = footerItem.getValue ();
            this.m_FooterTable.append ("<li><sup>(")
                    .append (this.m_UseFooterCount ? this.m_FooterCount : "*")
                    .append (")</sup> ")
                    .append (footer.getName ())
                    .append ("</li>")
                    .append (Html.Newline);
            String insertion = MessageFormat.format ("<span data-footerid=\"{1}\" class=\"footnote\"><sup>({0})</sup></span>",
                                                     this.m_FooterCount, footer.getId ());
            passageHtml = this.insert (passageHtml, index, insertion);
            this.m_FooterCount++;
         }
         this.m_FooterTable.append ("</ul></td></tr>").append (Html.Newline);
      }

      StringBuilder html = new StringBuilder ();
      html.append ("<div id=\"")
              .append (passage.getId ())
              .append ("\">")
              .append (Html.Newline);
      if (passage.getHeader () != null) {
         String header = passage.getHeader ().getName ();
         html.append ("<div id=\"header_")
                 .append (passage.getId ())
                 .append ("\"")
                 .append (header.contains ("<") ? "" : " class=\"header\"")
                 .append (" data-headerid=\"")
                 .append (passage.getHeader ().getId ())
                 .append ("\">")
                 .append (Html.Newline)
                 .append (header)
                 .append (Html.Newline)
                 .append ("</div>")
                 .append (Html.Newline);
      }
      html.append ("<div id=\"passage_")
              .append (passage.getId ())
              .append ("\">")
              .append (Html.Newline)
              .append (passageHtml.toString ())
              .append (Html.Newline)
              .append ("</div>")
              .append (Html.Newline)
              .append ("</div>")
              .append (Html.Newline);
      return html.toString ();
   }

   /**
    * Gets the footer table if any.
    * @return An HTML table with the footer information.
    */
   public String getFooterTable ()
   {
      if (this.m_FooterTable.length () > 0) {
         if (this.m_FooterTable.indexOf ("</table>") < 0) {
            this.m_FooterTable.append ("</table>")
                    .append (Html.Newline);
         }
      }
      return this.m_FooterTable.toString ();
   }

   /**
    * Gets the total length to add for the given index.
    * @param index Index to check.
    * @return The total length to add.
    */
   private int getAddedLength (int index)
   {
      return this.getAddedLength (index, false);
   }

   /**
    * Gets the total length to add for the given index.
    * @param index Index to check.
    * @param doBefore True to insert the insertion before any index that might be the same, otherwise false.
    * @return The total length to add.
    */
   private int getAddedLength (int index, boolean doBefore)
   {
      int addLength = 0;
      for (Map.Entry<Integer, Integer> insertion : this.m_Insertions.entrySet ()) {
         Integer insertIndex = insertion.getKey ();
         Integer addedLength = insertion.getValue ();
         if (doBefore) {
            if (insertIndex < index) {
               addLength += addedLength;
            }
         } else {
            if (insertIndex <= index) {
               addLength += addedLength;
            }
         }
      }
      return addLength;
   }

   /**
    * Adds insertion data for the given index.
    * @param index Index with insertion data.
    * @param length Length of insertion data.
    */
   private void addInsertion (int index, int length)
   {
      if (this.m_Insertions.containsKey (index)) {
         length += this.m_Insertions.get (index);
      }
      this.m_Insertions.put (index, length);
   }

   /**
    * Renders the given link to HTML.
    * @param link The link to render.
    * @return The HTML representation of the given link.
    */
   private static String renderLinkStart (Link link)
   {
      StringBuilder linkHtml = new StringBuilder ("<a href=\"\" onclick=\"app.");
      try {
         if (link.getArea ().equalsIgnoreCase ("glossary")) {
            linkHtml.append ("openGlossary ('")
                    .append (URLEncoder.encode (link.getLink (), "UTF-8"))
                    .append ("');\"")
                    .append (" title=\"")
                    .append (link.getLink ());
         } else if (link.getArea ().equalsIgnoreCase ("read")) {
            ReadLink read = new ReadLink (link.getLink ());
            linkHtml.append ("openRead ('")
                    .append (read.hasBook ()
                             ? URLEncoder.encode (read.getBook (), "UTF-8") : "")
                    .append ("', '")
                    .append (read.hasSubBook ()
                             ? URLEncoder.encode (read.getSubBook (), "UTF-8")
                             : "")
                    .append ("', '")
                    .append (read.hasChapter ()
                             ? URLEncoder.encode (read.getChapter (), "UTF-8")
                             : "")
                    .append ("', '")
                    .append (read.hasVerse ()
                             ? URLEncoder.encode (read.getVerse (), "UTF-8")
                             : "")
                    .append ("');\"")
                    .append (" title=\"")
                    .append (link.getLink ());
         } else if (link.getArea ().equalsIgnoreCase ("search")) {
            linkHtml.append ("openSearch ('")
                    .append (URLEncoder.encode (link.getLink (), "UTF-8"))
                    .append ("');\"")
                    .append (" class=\"link\" title=\"")
                    .append (link.getLink ());
         } else if (link.getArea ().equalsIgnoreCase ("external")) {
            linkHtml.append ("openExternal ('")
                    .append (URLEncoder.encode (link.getLink (), "UTF-8"))
                    .append ("');\"")
                    .append (" title=\"")
                    .append (link.getLink ());
         }
      } catch (UnsupportedEncodingException ex) {
         Dialogs.showErrorDialog (MainController.getInstance ().getStage (),
                                  "An error occured while encoding a link.",
                                  "Encoding error - PassageRenderer.renderLinkStart ()", "Error", ex);
      }
      linkHtml.append ("\">");

      return linkHtml.toString ();
   }
}
